package server;

import analyticsServer.AuctionEvent;
import analyticsServer.BidEvent;
import java.rmi.RemoteException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import server.data.*;

/**
 * Encapsulates all necessary operations which have to be done which Auctions.
 *
 * @author Andreas Mair
 */
public class AuctionManager {

    private static final String NEW_LINE=System.getProperty("line.separator");
    private static final SimpleDateFormat dateFormat=new SimpleDateFormat("dd.MM.yyyy HH:mm z");
    private static final NumberFormat numberFormat=DecimalFormat.getInstance();
    private static AuctionManager instance;
    private final Map<Long, Auction> auctions;
    private Timer timer;

    {
        numberFormat.setMaximumFractionDigits(2);
        numberFormat.setMinimumFractionDigits(2);
    }
    
    private AuctionManager() 
    {
        auctions = new HashMap<Long, Auction>();
        timer = new Timer();
    }
    
    public void initTimer()
    {
        timer=new Timer();
    }

    public static AuctionManager getInstance() {
        synchronized(AuctionManager.class)
        {
            if (instance == null) {
                instance = new AuctionManager();
            }
            return instance;
        }
    }

    /**
     * Returns a String representation of a date in Format "dd.MM.yyyy HH:mm z"
     *
     * @param date
     * @return String representation of date in Format "dd.MM.yyyy HH:mm z"
     */
    public synchronized String formatDate(final Date date) {
        return dateFormat.format(date);
    }

    /**
     * Returns a String representation of a double value using 2
     * fractiondigits
     *
     * @param double
     * @return String representation of a long value/100 using 2 fractiondigits
     */
    public String formatDouble(final double value) {
        return numberFormat.format(value);
    }

    /**
     * Returns a double representation of a String (containing a double) (2
     * fractiondigits)
     *
     * @param value
     * @return long representation of a String (containing a double)*100
     */
    public double parseDouble(final String value) throws ParseException {
        return  numberFormat.parse(value).doubleValue();
    }

    /**
     * Creates a new Auction
     *
     * @param duration long
     * @param description String
     * @param owner User
     * @return created Auction
     */
    public synchronized Auction create(final long duration, final String description, final User owner) {
        final Auction auction = new Auction(duration, description, owner);
        auctions.put(auction.getlId(), auction);
        timer.schedule(new AuctionTask(auction.getlId()), duration * 1000);
        Server.callAnalyticsServer(new AuctionEvent(Server.NAME, AuctionEvent.AUCTION_STARTED, System.currentTimeMillis(), auction.getlId()));

        return auction;
    }

    /**
     * Bids on an Auction, informs AnalyticsServer
     *
     * @param auctionId int
     * @param lPrice long
     * @param bidder User
     * @return Auction or null if there is no Auction with auctionId
     */
    public synchronized Auction bid(final Bid bid) throws RemoteException {
        final Auction auction = this.auctions.get(bid.getAuctionId());        
        if(auction != null && !auction.isFinished())
        {
            if (auction.overbidBy(bid)) {
                if (auction.getHighestBid() != null) {//if there was a bid and it was not from the current bidder
                    Server.callAnalyticsServer(new BidEvent(Server.NAME, BidEvent.BID_OVERBID, System.currentTimeMillis(), bid));
                    //UserManager.getInstance().sendUDPMessage(auction.getuHighest(), "!new-bid " + auction.getStrDesc());
                }
                auction.setHighestBid(bid);
                bid.setState(Bid.OVERBID);
                Server.callAnalyticsServer(new BidEvent(Server.NAME, BidEvent.BID_PLACED, System.currentTimeMillis(), bid));
            } else {
                bid.setState(Bid.UNDERBID);
            } 
        }
        else
        {
            bid.setState(Bid.NO_AUCTION);
        }
        return auction;        
    }
    
    /**
     * Bids on an Auction, informs AnalyticsServer
     *
     * @param auctionId int
     * @param lPrice long
     * @param bidder User
     * @return Auction or null if there is no Auction with auctionId
     */
    public synchronized Auction bid(final SignedBid bid) throws RemoteException {
        final Auction auction = this.auctions.get(bid.getAuctionId());        
        if(auction != null && auction.getdEnd().before(new Date(bid.getAverageTimeStamp())))
        {
            if (auction.overbidBy(bid)) {
                if (auction.getHighestBid() != null) {//if there was a bid and it was not from the current bidder
                    Server.callAnalyticsServer(new BidEvent(Server.NAME, BidEvent.BID_OVERBID, System.currentTimeMillis(), bid));
                    //UserManager.getInstance().sendUDPMessage(auction.getuHighest(), "!new-bid " + auction.getStrDesc());
                }
                auction.setHighestBid(bid);
                bid.setState(Bid.OVERBID);
                Server.callAnalyticsServer(new BidEvent(Server.NAME, BidEvent.BID_PLACED, System.currentTimeMillis(), bid));
            } else {
                bid.setState(Bid.UNDERBID);
            } 
        }
        else
        {
            bid.setState(Bid.NO_AUCTION);
        }
        return auction;        
    }    
    
    /**
     * Bids on an Auction, informs AnalyticsServer
     *
     * @param bid GroupBid
     * @return Auction or null if there is no Auction with auctionId
     */
    public synchronized Auction groupBid(final GroupBid bid) throws RemoteException {
        final Auction auction = this.auctions.get(bid.getAuctionId());
        if(auction != null && !auction.isFinished())
        {
            if (auction.overbidBy(bid)) 
            {
                auction.addGroupBid(bid);
                bid.setState(Bid.OVERBID);
            } else {
                bid.setState(Bid.UNDERBID);
            }
            
        }
        else
        {
            bid.setState(Bid.NO_AUCTION);
        }
        return auction;    
    }
    
    /**
     * Confirms a GroupBid on an Auction
     *
     * @param confirmation Confirmation
     * @return Auction or null if there is no Auction with auctionId
     */
    public synchronized void confirm(final Confirmation confirmation){
        final Auction auction = this.auctions.get(confirmation.getAuctionId());
        GroupBid groupBid;
        if(auction != null && !auction.isFinished() && (groupBid=auction.getGroupBid(confirmation.toAuctionString()))!=null)
        {
            if (! groupBid.getBidder().toString().equalsIgnoreCase(confirmation.getConfirmer().toString())) {
                if(groupBid.isTentative())
            {
                groupBid.addConfirmation(confirmation);
                if(groupBid.isTentative()) //if it was just the first confirmation setup a TimerThread which ends the grouBid after 20 secods if its not finished before
                {
                    timer.schedule(new GroupBidTask(groupBid.toString()), 20000);
                }
                else //GroupBid has to be finished now ==> stop TimerThread and send !confirmed (if bid is higher than current)
                {
                    groupBid.setFinished(true);
                    try
                    {
                        this.bid(groupBid);
                    }catch(RemoteException ex)
                    {
                        //ignored in this Task
                    }
                    String strMessage="!confirmed";
                    if(groupBid.getState()==Bid.UNDERBID)
                    {
                        strMessage="!rejected Please use a higher bid price.";
                    }
                    List<User> users=groupBid.getUsers();
                    for(User user:users)
                    {
                        UserManager.getInstance().addNotification(user, strMessage);
                    }
                    auction.removeGroupBid(groupBid);
                }
            }
            else
            {
                UserManager.getInstance().addNotification(confirmation.getConfirmer(), "!rejected");
            }
            } else {
                UserManager.getInstance().addNotification(confirmation.getConfirmer(), "!rejected Can't confirm own bid.");
            }
                        
        }
        else
        {
            UserManager.getInstance().addNotification(confirmation.getConfirmer(), "!rejected");
        }
    }       

    /**
     * Called on end of Auction. Ends auction, informs BillingServer and AnalyticsServer, informs
     * users.
     *
     * @param auctionId
     * @throws RemoteException
     */
    public void finishAuction(final long auctionId) {
            Auction auction;

            synchronized (this) {
                auction = this.auctions.remove(auctionId);
            }

                Server.callBillingServer(auction.getHighestBid());
                Server.callAnalyticsServer(new AuctionEvent(Server.NAME, AuctionEvent.AUCTION_ENDED, System.currentTimeMillis(), auctionId));


//            String strPrice = formatLong(auction.getlPrice());
            if (auction.getHighestBid() != null) {
                Server.callAnalyticsServer(new BidEvent(Server.NAME, BidEvent.BID_WON, System.currentTimeMillis(), auction.getHighestBid()));
                //UserManager.getInstance().sendUDPMessage(auction.getuHighest(), "!auction-ended " + auction.getuHighest().getStrName() + " " + strPrice + " " + auction.getStrDesc());
                //UserManager.getInstance().sendUDPMessage(auction.getuOwner(), "!auction-ended " + auction.getuHighest().getStrName() + " " + strPrice + " " + auction.getStrDesc());
            }/* else {
                UserManager.getInstance().sendUDPMessage(auction.getuOwner(), "!auction-ended none " + strPrice + " " + auction.getStrDesc());
            }*/
    }
    
    /**
     * Called when time of a GroupBid is over. Cancels groupBid, informs users.
     *
     * @param groupBid String identifies a groupbid (contains out of auction_id+" "+value+" "+username)
     */
    public synchronized void finishGroupBid(final String strGroupBid) {
        //strgroupBid ist zu kurz um Confirmation zu erstellen->einfach String inkl. Space angehängt
        
        try {
            //@todo remove groupBid, notify users
            
            //in Auctions verweis auf tentative groupBids um diese zu canceln (bei Auktionsablauf)
            //in Auctions groupBid counter (erhöht wenn tentative, vermindert wenn gecancelt (wenn abgeschlossen nicht))
            Confirmation tmp=new Confirmation(strGroupBid.split(" "), null);
            final Auction auction = this.auctions.get(tmp.getAuctionId());
            GroupBid groupBid;
            if(auction != null && !auction.isFinished() && (groupBid=auction.getGroupBid(tmp.toAuctionString()))!=null && !groupBid.isFinished())
            {
                List<User> users=groupBid.getUsers();
                for(User user:users)
                {
                    UserManager.getInstance().addNotification(user, "!rejected");
                }
                auction.removeGroupBid(groupBid);
            }
                
            
            
        } catch (InvalidCommandException ex) {
            Logger.getLogger(AuctionManager.class.getName()).log(Level.SEVERE, null, ex);
        }
        
    }    

    /**
     * Lists all Auctions
     *
     * @return String representation of all Auctions of "No auctions available!"
     */
    public synchronized String list() {
        StringBuilder strRet;
        if (auctions.isEmpty()) {
            strRet=new StringBuilder("No auctions available!");
        } else {
            strRet = new StringBuilder(auctions.size()*40);
            for (Auction a : auctions.values()) {
                if(strRet.length()>0)
                {
                    strRet.append(NEW_LINE);
                }
                strRet.append(a);
            }
        }
        return strRet.toString();
    }
    
    /**
     * Returns the number of Auctions with GroupBids
     *
     * @return number of auctions with GroupBids
     */
    public synchronized int getAuctionWGroupBidCount() {
        int iRet = 0;
        for (Auction auction : this.auctions.values()) {
            if (auction.hasGroupBid()) {
                iRet++;
            }
        }
        return iRet;
    }
    
    

    /**
     * Stops the Timer
     */
    public synchronized void stopTimer() {
        this.timer.cancel();
    }

    private class AuctionTask
            extends TimerTask {

        private final long auctionId;

        public AuctionTask(final long auctionId) {
            super();
            this.auctionId = auctionId;
        }

        @Override
        public void run() {
            finishAuction(auctionId);
        }
    }
    
    private class GroupBidTask
            extends TimerTask {

        private final String groupBid;

        /**
         * Creates a GroupBidTask (to finish a GroupBid after max. 20 Seconds)
         * @param groupBid identifies a groupbid (contains out of auction_id+" "+value+" "+username)
         */
        public GroupBidTask(final String groupBid) {
            super();
            this.groupBid = "groupBid " + groupBid;
        }

        @Override
        public void run() {
            finishGroupBid(groupBid);
        }
    }    
}
